from loguru import logger
from pymongo import *
from pymongo.errors import ConnectionFailure
from backend.settings import MONGO_URI_DEFAULT, MONGO_DB

def new_mongo_client(uri, **kwargs):
    """Create new pymongo.mongo_client.MongoClient instance. DO NOT USE IT DIRECTLY."""

    try:
        client = MongoClient(uri, maxPoolSize=1024, **kwargs)
        client.admin.command('ismaster')  # The ismaster command is cheap and does not require auth.
    except ConnectionFailure:
        logger.error("new_mongo_client(): Server not available, Please check you uri: {}".format(uri))
        return None
    else:
        logger.info(f"get mongodb connection client from {uri}")
        return client


URI_CLIENT_DICT = {}  # a dictionary hold all client with uri as key


def get_mongo_client(uri=MONGO_URI_DEFAULT, fork=False, **kwargs):
    """Get pymongo.mongo_client.MongoClient instance. One mongodb uri, one client.

    @:param uri: mongodb uri
    @:param fork: for fork-safe in multiprocess case, if fork=True, return a new MongoClient instance, default False.
    @:param kwargs: refer to pymongo.mongo_client.MongoClient kwargs
    """
    if fork:
        return new_mongo_client(uri, **kwargs)
    global URI_CLIENT_DICT
    matched_client = URI_CLIENT_DICT.get(uri)
    if matched_client is None:  # no matched client
        new_client = new_mongo_client(uri, **kwargs)
        if new_client is not None:
            URI_CLIENT_DICT[uri] = new_client
        return new_client
    return matched_client


def get_mongo_database(db=MONGO_DB):
    """
    单实例模式
    :param db: 数据库名称，默认pctt
    :return: 数据库连接对象
    """
    return get_mongo_client()[db]


def get_mongo_collection(tableName):
    return get_mongo_database()[tableName]


'''
class DBManager:
    """A safe and simple pymongo packaging class ensuring existing database and collection.

    Operations:
    MongoClient level operations: https://api.mongodb.com/python/current/api/pymongo/mongo_client.html
    Database level operations: https://api.mongodb.com/python/current/api/pymongo/database.html
    Collection level operations: https://api.mongodb.com/python/current/api/pymongo/collection.html
    """
    __default_uri = 'mongodb://localhost:27017/admin'
    __default_db_name = 'test'
    __default_coll_name = 'test'

    def __init__(self, uri=__default_uri, db_name=__default_db_name, coll_name=__default_coll_name, **kwargs):
        self.__uri = uri
        self.__db_name = db_name
        self.__coll_name = coll_name
        self.__client = get_mongo_client(uri, **kwargs)
        self.__db = get_existing_db(self.__client, db_name)
        self.__coll = get_existing_coll(self.__db, coll_name)

    def __str__(self):
        return u'uri: {}, db_name: {}, coll_name: {}, id_client: {}, client: {}, db: {}, coll: {}'.format(
            self.uri, self.db_name, self.coll_name, id(self.client), self.client, self.db, self.coll)

    @property
    def uri(self):
        return self.__uri

    @property
    def db_name(self):
        return self.__db_name

    @property
    def coll_name(self):
        return self.__coll_name

    @db_name.setter
    def db_name(self, db_name):
        self.__db_name = db_name
        self.__db = get_existing_db(self.__client, db_name)

    @coll_name.setter
    def coll_name(self, coll_name):
        self.__coll_name = coll_name
        self.__coll = get_existing_coll(self.__db, coll_name)

    @property
    def client(self):
        return self.__client

    @property
    def db(self):
        return self.__db

    @property
    def coll(self):
        # always use the current instance self.__db
        self.__coll = get_existing_coll(self.__db, self.__coll_name)
        return self.__coll

    def create_coll(self, db_name, coll_name):
        """Create new collection with new or existing database"""
        if self.__client is None:
            return None
        try:
            return self.__client.get_database(db_name).create_collection(coll_name)
        except CollectionInvalid:
            logging.error('collection {} already exists in database {}'.format(coll_name, db_name))
            return None

    def session_pipeline(self, pipeline):
        if self.__client is None:
            logging.error('client is None in session_pipeline: {}'.format(self.__client))
            return None
        with self.__client.start_session(causal_consistency=True) as session:
            result = []
            for operation in pipeline:
                try:
                    if operation.level == 'client':
                        target = self.__client
                    elif operation.level == 'db':
                        target = self.__db
                    elif operation.level == 'coll':
                        target = self.__coll

                    operation_name = operation.operation_name
                    args = operation.args
                    kwargs = operation.kwargs
                    operator = getattr(target, operation_name)
                    if type(args) == tuple:
                        ops_rst = operator(*args, session=session, **kwargs)
                    else:
                        ops_rst = operator(args, session=session, **kwargs)

                    if operation.callback is not None:
                        operation.out = operation.callback(ops_rst)
                    else:
                        operation.out = ops_rst

                except Exception as e:
                    logging.error('{} {} Exception, session_pipeline args: {}, kwargs: {}'.format(
                        target, operation, args, kwargs))
                    logging.error('session_pipeline Exception: {}'.format(repr(e)))
                result.append(operation)
            return result

    # https://api.mongodb.com/python/current/api/pymongo/client_session.html#transactions
    def transaction_pipeline(self, pipeline):
        if self.__client is None:
            logging.error('client is None in transaction_pipeline: {}'.format(self.__client))
            return None
        with self.__client.start_session(causal_consistency=True) as session:
            with session.start_transaction():
                result = []
                for operation in pipeline:
                    try:
                        if operation.level == 'client':
                            target = self.__client
                        elif operation.level == 'db':
                            target = self.__db
                        elif operation.level == 'coll':
                            target = self.__coll
                        operation_name = operation.operation_name
                        args = operation.args
                        kwargs = operation.kwargs
                        operator = getattr(target, operation_name)
                        if type(args) == tuple:
                            ops_rst = operator(*args, session=session, **kwargs)
                        else:
                            ops_rst = operator(args, session=session, **kwargs)

                        if operation.callback is not None:
                            operation.out = operation.callback(ops_rst)
                        else:
                            operation.out = ops_rst
                    except Exception as e:
                        logging.error('{} {} Exception, transaction_pipeline args: {}, kwargs: {}'.format(
                            target, operation, args, kwargs))
                        logging.error('transaction_pipeline Exception: {}'.format(repr(e)))
                        raise Exception(repr(e))
                    result.append(operation)
                return result
'''
